/*
* Copyright 2008 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.fulltext;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import ome.io.nio.OriginalFilesService;
import ome.model.IAnnotated;
import ome.model.ILink;
import ome.model.IObject;
import ome.model.annotations.Annotation;
import ome.model.annotations.FileAnnotation;
import ome.model.annotations.MapAnnotation;
import ome.model.annotations.TagAnnotation;
import ome.model.annotations.TextAnnotation;
import ome.model.annotations.TermAnnotation;
import ome.model.core.OriginalFile;
import ome.model.internal.Details;
import ome.model.internal.NamedValue;
import ome.model.internal.Permissions;
import ome.model.meta.Event;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.util.DetailsFieldBridge;
import ome.util.Utils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.hibernate.search.bridge.FieldBridge;
import org.hibernate.search.bridge.LuceneOptions;
import org.hibernate.search.bridge.builtin.DateBridge;
/**
* Primary definition of what will be indexed via Hibernate Search. This class
* is delegated to by the {@link DetailsFieldBridge}, and further delegates to
* classes as defined under "SearchBridges".
*
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta3
* @see <a href="http://trac.openmicroscopy.org.uk/ome/FileParsers">Parsers</a
* href>
* @see <a href="http://trac.openmicroscopy.org.uk/ome/SearchBridges">Bridges</a
* href>
*/
public class FullTextBridge extends BridgeHelper {
// TODO insert/update OR delete regular type OR annotated type OR originalfile
final protected OriginalFilesService files;
final protected Map<String, FileParser> parsers;
final protected Class<FieldBridge>[] classes;
/**
* Since this constructor provides the instance with no way of parsing
* {@link OriginalFile} binaries, all files will be assumed to have blank
* content. Further, no custom bridges are provided and so only the default
* indexing will take place.
*/
public FullTextBridge() {
this(null, null);
}
/**
* Constructor which provides an empty set of custom
* {@link FieldBridge bridges}.
*/
@SuppressWarnings("unchecked")
public FullTextBridge(OriginalFilesService files,
Map<String, FileParser> parsers) {
this(files, parsers, new Class[] {});
}
/**
* Main constructor.
*
* @param files
* {@link OriginalFilesService} for getting access to binary files.
* @param parsers
* List of {@link FileParser} instances which are currently
* configured.
* @param bridgeClasses
* set of {@link FieldBridge bridge classes} which will be
* instantiated via a no-arg constructor.
* @see <a
* href="http://trac.openmicroscopy.org.uk/ome/SearchBridges">Bridges</a
* href>
*/
@SuppressWarnings("unchecked")
public FullTextBridge(OriginalFilesService files,
Map<String, FileParser> parsers, Class<FieldBridge>[] bridgeClasses) {
this.files = files;
this.parsers = parsers;
this.classes = bridgeClasses == null ? new Class[] {} : bridgeClasses;
}
/**
* Default implementation of the
* {@link #set(String, Object, Document, LuceneOptions)}
* method which calls
* {@link #set_file(String, IObject, Document, LuceneOptions)}
* {@link #set_annotations(String, IObject, Document, LuceneOptions)},
* {@link #set_details(String, IObject, Document, LuceneOptions)},
* and finally
* {@link #set_custom(String, IObject, Document, LuceneOptions)}.
* as well as all {@link Annotation annotations}.
*/
@Override
public void set(String name, Object value, Document document, LuceneOptions opts) {
IObject object = (IObject) value;
// Store class in COMBINED
String cls = Utils.trueClass(object.getClass()).getName();
add(document, null, cls, opts);
set_file(name, object, document, opts);
set_annotations(name, object, document, opts);
set_details(name, object, document, opts);
set_custom(name, object, document, opts);
}
/**
* Uses {@link BridgeHelper#parse(OriginalFile, OriginalFilesService, Map)}
* to get a {@link Reader} for the given
* file which is then passed to
* {@link #addContents(Document, String, OriginalFile, OriginalFilesService, Map, LuceneOptions)}
* using the field name "file".
*
* @param name
* @param object
* @param document
* @param opts
*/
public void set_file(final String name, final IObject object,
final Document document, final LuceneOptions opts) {
if (object instanceof OriginalFile) {
OriginalFile file = (OriginalFile) object;
addContents(document, "file.contents", file, files, parsers, opts);
}
}
/**
* Walks the various {@link Annotation} instances attached to the object
* argument and adds various levels to the index.
*
* @param name
* @param object
* @param document
* @param opts
*/
public void set_annotations(final String name, final IObject object,
final Document document, final LuceneOptions opts) {
if (object instanceof ILink) {
ILink link = (ILink) object;
if (link.getChild() instanceof Annotation) {
reindex(link.getParent());
}
}
if (object instanceof IAnnotated) {
IAnnotated annotated = (IAnnotated) object;
List<Annotation> list = annotated.linkedAnnotationList();
for (Annotation annotation : list) {
String at = annotationTypeString(annotation);
add(document, "annotation.type", at, opts);
if (annotation.getName() != null) {
add(document, "annotation.name", annotation.getName(), opts);
}
if (annotation.getNs() != null) {
add(document, "annotation.ns", annotation.getNs(), opts);
}
if (annotation instanceof TermAnnotation) {
TermAnnotation term = (TermAnnotation) annotation;
String termValue = term.getTermValue();
termValue = termValue == null ? "" : termValue;
add(document, "term", termValue, opts);
} else if (annotation instanceof TextAnnotation) {
TextAnnotation text = (TextAnnotation) annotation;
String textValue = text.getTextValue();
textValue = textValue == null ? "" : textValue;
add(document, "annotation", textValue, opts);
if (annotation instanceof TagAnnotation) {
add(document, "tag", textValue, opts);
List<Annotation> list2 = annotation
.linkedAnnotationList();
for (Annotation annotation2 : list2) {
if (annotation2 instanceof TextAnnotation) {
TextAnnotation text2 = (TextAnnotation) annotation2;
String textValue2 = text2.getTextValue();
textValue2 = textValue2 == null ? ""
: textValue2;
add(document, "annotation", textValue2, opts);
}
}
}
} else if (annotation instanceof FileAnnotation) {
FileAnnotation fileAnnotation = (FileAnnotation) annotation;
handleFileAnnotation(document, opts, fileAnnotation);
} else if (annotation instanceof MapAnnotation) {
MapAnnotation mapAnnotation = (MapAnnotation) annotation;
handleMapAnnotation(document, opts, mapAnnotation);
}
}
}
// Have to be careful here, since Annotations are also IAnnotated.
// Don't use if/else
if (object instanceof FileAnnotation) {
FileAnnotation fileAnnotation = (FileAnnotation) object;
handleFileAnnotation(document, opts, fileAnnotation);
} else if (object instanceof MapAnnotation) {
MapAnnotation mapAnnotation = (MapAnnotation) object;
handleMapAnnotation(document, opts, mapAnnotation);
}
}
/**
* Parses all ownership and time-based details to the index for the given
* object.
*
* @param name
* @param object
* @param document
* @param opts
*/
public void set_details(final String name, final IObject object,
final Document document, final LuceneOptions opts) {
final LuceneOptions stored = new SimpleLuceneOptions(opts, Store.YES);
final LuceneOptions storedNotAnalyzed = new SimpleLuceneOptions(opts, Index.NOT_ANALYZED, Store.YES);
Details details = object.getDetails();
if (details != null) {
Experimenter e = details.getOwner();
if (e != null && e.isLoaded()) {
String omename = e.getOmeName();
String firstName = e.getFirstName();
String lastName = e.getLastName();
add(document, "details.owner.omeName", omename, stored);
add(document, "details.owner.firstName", firstName, opts);
add(document, "details.owner.lastName", lastName, opts);
}
ExperimenterGroup g = details.getGroup();
if (g != null && g.isLoaded()) {
String groupName = g.getName();
add(document, "details.group.name", groupName, stored);
}
Event creationEvent = details.getCreationEvent();
if (creationEvent != null) {
add(document, "details.creationEvent.id", creationEvent.getId()
.toString(), storedNotAnalyzed);
if (creationEvent.isLoaded()) {
String creation = DateBridge.DATE_SECOND
.objectToString(creationEvent.getTime());
add(document, "details.creationEvent.time", creation,
storedNotAnalyzed);
}
}
Event updateEvent = details.getUpdateEvent();
if (updateEvent != null) {
add(document, "details.updateEvent.id", updateEvent.getId()
.toString(), storedNotAnalyzed);
if (updateEvent.isLoaded()) {
String update = DateBridge.DATE_SECOND
.objectToString(updateEvent.getTime());
add(document, "details.updateEvent.time", update,
storedNotAnalyzed);
}
}
Permissions perms = details.getPermissions();
if (perms != null) {
add(document, "details.permissions", perms.toString(), stored);
}
}
}
/**
* Loops over each {@link #classes field bridge class} and calls its
* {@link FieldBridge#set(String, Object, Document, LuceneOptions)}
* method. Any exceptions are logged but do not cancel execution.
*
* @param name
* @param object
* @param document
* @param opts
*/
public void set_custom(final String name, final IObject object,
final Document document, final LuceneOptions opts) {
for (Class<FieldBridge> bridgeClass : classes) {
if (bridgeClass != null) {
FieldBridge bridge = null;
try {
bridge = bridgeClass.newInstance();
if (bridge instanceof BridgeHelper) {
BridgeHelper helper = (BridgeHelper) bridge;
helper.setApplicationEventPublisher(publisher);
}
bridge.set(name, object, document, opts);
} catch (Exception e) {
final String msg = String
.format(
"Error calling set on custom bridge type:%s; instance:%s",
bridgeClass, bridge);
logger().error(msg, e);
}
}
}
}
/**
* Creates {@link Field} instances for {@link FileAnnotation} objects.
*
* @param document
* @param opts
* @param fileAnnotation
*/
private void handleFileAnnotation(final Document document,
final LuceneOptions opts, FileAnnotation fileAnnotation) {
OriginalFile file = fileAnnotation.getFile();
if (file != null) {
// None of these values can be null
add(document, "file.name", file.getName(), opts);
add(document, "file.path", file.getPath(), opts);
if (file.getHasher() != null) {
add(document, "file.hasher", file.getHasher().getValue(), opts);
}
if (file.getHash() != null) {
add(document, "file.hash", file.getHash(), opts);
}
if (file.getMimetype() != null) {
add(document, "file.format", file.getMimetype(), opts);
// ticket:2211 - duplicating for backwards compatibility
add(document, "file.mimetype", file.getMimetype(), opts);
}
addContents(document, "file.contents", file, files, parsers, opts);
}
}
/**
* Creates {@link Field} instances for {@link MapAnnotation} named-value
* pair.
*
* @param document
* @param opts
* @param mapAnnotation
*/
private void handleMapAnnotation(final Document document,
final LuceneOptions opts, MapAnnotation mapAnnotation) {
List<NamedValue> nvs = mapAnnotation.getMapValue();
if (nvs != null && nvs.size() > 0) {
for (NamedValue nv : nvs) {
if (nv != null) {
add(document, nv.getName(), nv.getValue(), opts);
add(document, "has_key", nv.getName(), opts);
add(document, "annotation", nv.getValue(), opts);
add(document, "annotation", nv.getName(), opts);
}
}
}
}
/**
* Return the short type name of an {@link Annotation}. If the instance is
* an {@link ome.model.annotations.TextAnnotation} the returned value will
* be "TextAnnotation".
*
* @param annotation
* @return See above.
*/
private String annotationTypeString(Annotation annotation) {
Class ac = Utils.trueClass(annotation.getClass());
int dot = ac.getName().lastIndexOf('.');
if (dot < 0) {
dot = -1;
}
String at = ac.getName().substring(dot + 1, ac.getName().length());
return at;
}
}